iT邦幫忙

2025 iThome 鐵人賽

DAY 3
0

前情提要

昨天我們已經把 LLM 的訓練訓練跑了個大概,但也就只是跑起來還沒開始學習,今天就來細看第一步吧。

主要是以我有實作的 github 當作例子,所以比較會偏向 ASR 及 TTS,有興趣的可以看看。

1. tokenizer

核心觀念: 文字 → 數字
tokenizer (分詞器) 的想法其實很簡單,因為不管是中文字還英文字都沒辦法直接丟進去給模型做訓練,所以 tokenizer 會透過辭典將單詞轉換成"數字",轉後過後才可以當輸入送進模型,然而數字從 0 開始 (可以看從 model/tokenizer.json)。
Q: 那至於現在的 LLM 辭典大小都是多少呢??
A: 我們參考這篇論文 SMALL LANGUAGE MODELS:
SURVEY, MEASUREMENTS, AND INSIGHTS
(這篇論文的統計之後還會提到),當中的圖表有統計 SLM 的部分,每年的趨勢可以當作參考,畢竟辭典大小會影響到模型大小和訓練的難易度,太大不好太小也不好,所以各家模型都會有些微差異(可參考論文 Table 1)。
  https://ithelp.ithome.com.tw/upload/images/20250828/20168446vbDJW3LtEs.png

Q: 那在 ASR 跟 TTS 辭典又是多大呢??
A: 我們以現有的模型來看

  • ASR (nemo canary) 大概會是以下這樣,通常會是 512, 1024 的倍數,那中文因為是以字為單位,所以需要比較多
    英, 法, 德: 1024
    韓: 3072
    日, 中: 8192

  • TTS 的部分主要是以拼音為辭典,以 MeloTTS 多個外國語言大概一兩百個,那如果以中文來說通常使用 pinyin 或 g2pW 大概落在 1550 左右( ZipVoice , F5-TTS)。

Q: 有哪些 tokenizer 的演算法呢??
A: 主要是 bpe, SentencePiece, Unigram, bbpe …
(補充: 在中文 ASR 採用 bbpe 效果更好,可參考這篇講解)

2. 程式補充

以 scripts/train_tokenizer.py 當中其實就是用 tokenizers 這個 package 來訓練,基本上就幾行程式而已,這邊就不過多敘述,稍微補充幾種 special_tokens。


# 定義特殊token, 基本上可以定義你想要的

# 多模態的 
# https://huggingface.co/microsoft/Phi-4-multimodal-instruct/blob/main/tokenizer_config.json 

# 如果是多語言 asr 會有 language tag
# https://huggingface.co/openai/whisper-large-v3/raw/main/tokenizer_config.json 

# 如果是 ASR + LLM
# https://huggingface.co/VITA-MLLM/VITA-Audio-Boost/raw/main/tokenizer_config.json (拉到最底)
special_tokens = ["<|endoftext|>", "<|im_start|>", "<|im_end|>"]


3. bpe tokenzier 輸出長怎樣??

我們先簡單認識一下分詞完的結果長怎樣,後面會有詳細介紹
我們在 scripts/train_tokenizer.py 修改一下程式,打印一下輸出,那因為是用 ByteLevel,所以中文字會是看不懂的符號,那英文 system → [s, ystem]

def eval_tokenizer():
    from transformers import AutoTokenizer

    # 加载预训练的tokenizer
    tokenizer = AutoTokenizer.from_pretrained("../model/")

    messages = [
        {"role": "system", "content": "你是一个优秀的聊天机器人,总是给我正确的回应!"},
        # {"role": "user", "content": '你来自哪里?'},
        # {"role": "assistant", "content": '我来自地球'}
    ]
    new_prompt = tokenizer.apply_chat_template(
        messages,
        tokenize=False
    )
    print(new_prompt)

    # 获取实际词汇表长度(包括特殊符号)
    actual_vocab_size = len(tokenizer)
    print('tokenizer实际词表长度:', actual_vocab_size)

    # model_inputs 會回傳 input_ids, token_type_ids, attention_mask
    model_inputs = tokenizer(new_prompt)

    # 其中 input_ids 是已經轉成數字格式
    # 我們可以透過 .tokenize 看到分詞結果
    # 再透過 convert_tokens_to_ids 看到原先的 input_ids
    tokens = tokenizer.tokenize(new_prompt)
    input_ids = tokenizer.convert_tokens_to_ids(tokens)
    print('encoder长度:', len(model_inputs['input_ids']), model_inputs)
    print(f'\ntokens: {tokens}, input_ids: {input_ids}')
 

    input_ids = model_inputs['input_ids']
    response = tokenizer.decode(input_ids, skip_special_tokens=False)
    print('decoder和原始文本是否一致:', response == new_prompt)


def main():
    # train_tokenizer()
    eval_tokenizer()

https://ithelp.ithome.com.tw/upload/images/20250828/20168446Yo5WaAY1k2.png

4.0 分詞方式, 分詞粒度

參考文章: https://zhuanlan.zhihu.com/p/652520262 ,參考文章當中寫得非常詳細,底下只是做些總結及整理。
依照精細程度分成: 詞 word-based < 子詞 subword-based < 字符 character-based
以下舉個例子就可以明白

word-based char-based subwrd-based
中文 我喜歡看電影 我 | 喜歡 | 看 | 電影 我 | 喜 | 歡 | 看 | 電 | 影 X
英文 Let us learn tokenization. [“Let”, “us”, “learn”, “tokenization.”] [“L”, “e”, “t”, “u”, “s”, “l”, “e”, “a”, “r”, “n”, “t”, “o”, “k”, “e”, “n”, “i”, “z”, “a”, “t”, “i”, “o”, “n”, “.”] [“Let”, “us”, “learn”, “token”, “ization.”]
優點 1. 語義明確 2. 上下文理解 1. 簡單暴力,各語言處理方式一樣 2. 不會有 OOV 問題 1. 兼具前兩種優點
缺點 1. 辭典要很大, 但一定會有漏掉的詞,就會產生 OOV(Out-of-Vocabulary) 2. 限制模型的理解能力,比如 learn 和 learning 在辭典會是兩個詞 1. 英文語義不明確 2. 效率差記憶體大,因為每個 token 都需要 memory,所以越多 token 效率越差記憶體大計算量大。 1. 但中文沒辦法用 (所以才有 bbpe 針對中文這種)

4.1 WordPiece

這裡整理參考文章的 code,我自己也是跟著寫一遍
核心觀念: 單詞 → 多個前綴符號(類似 bert 中的 ##) → 慢慢合併成子詞
舉例來說: tokenization→ t ##o ##k ##e … → token #ization
會有以下步驟(來至於參考文章):
https://ithelp.ithome.com.tw/upload/images/20250828/20168446wPC0o4RSGf.png

def WordPiece(sentences, vocab_size = 50):
    # Step 1: 計算初始詞表
    
    # Step 1.1: 計算詞頻
    stats = build_stats(sentences)
    # Step 1.2 建立初始詞表
    #['##a', '##e', '##g', '##h', ..., 'I', 'S', ..., '不', '他', '吃', '喜', '我', '苹']
    vocab = build_init_alphabet(stats)

    # splits: {'我': ['我'], '喜欢': ['喜', '##欢'], ...
    splits = split_stats(stats)
    while len(vocab) < vocab_size:
        # Step 2: 計算合併分數
        pair_scores = compute_pair_scores(stats, splits)

        # Step 3.1: 找到最高的分數
        best_pair, max_score = "", None
        for pair, score in pair_scores.items():
            if max_score is None or max_score < score:
                best_pair = pair
                max_score = score

        # step 3.2: 合併分數最高的子詞對
        splits = merge_pair(*best_pair, stats, splits)

        # step 3.3: 更新詞表
        new_token = (
            best_pair[0] + best_pair[1][2:]
            if best_pair[1].startswith("##")
            else best_pair[0] + best_pair[1]
        )
        vocab.append(new_token)

    return vocab


def main():
    sentences = [
        "我",
        "喜欢",
        "吃",
        "苹果",
        "他",
        "不",
        "喜欢",
        "吃",
        "苹果派",
        "I like to eat apples",
        "She has a cute cat",
        "you are very cute",
        "give you a hug",
    ]
    vocab = WordPiece(sentences, vocab_size = 50)
    print(vocab)

if __name__ == "__main__":
    main()

build_stats: 用 dict 來存放次數的統計

from collections import defaultdict

# stats: defaultdict(<class 'int'>, {'我': 1, '喜欢': 2, '吃': 2, '苹果': 1, '他': 1, '不': 1, '苹果派': 1, 
# 'I': 1, 'like': 1, 'to': 1, 'eat': 1, 'apples': 1, 'She': 1, 'has': 1, 'a': 2, 'cute': 2, 'cat': 1, 'you': 2, 'are': 1, 'very': 1, 'give': 1, 'hug': 1})
def build_stats(sentences):
    stats = defaultdict(int)
    for sent in sentences:
        # 英文用空格切成單個詞
        # 中文事先斷詞 -> 否則可用 jieba, ckip
        words = sent.split() 
        for word in words:
            stats[word] += 1
    return stats

build_init_alphabet: ##是個特殊符號,代表是從詞拆出來的中間或尾部 (ex: 喜歡 → 喜, ##歡)

#['##a', '##e', '##g', '##h', ..., 'I', 'S', ..., '不', '他', '吃', '喜', '我', '苹']
def build_init_alphabet(stats):
    alphabet = []
    for word in stats.keys():
        if word[0] not in alphabet:
            alphabet.append(word[0])
        for letter in word[1: ]:
            if f"##{letter}" not in alphabet:
                alphabet.append(f'##{letter}')  

    alphabet.sort()
    return alphabet

split_stats: 將原本統計的詞頻,建立每個詞的初始分割

# splits: {'我': ['我'], '喜欢': ['喜', '##欢'], ...
def split_stats(stats):
    splits = {
        word: [c if i == 0 else f"##{c}" for i, c in enumerate(word)]
        for word in stats.keys()
    }

    # 上面是將 for 合成一行相當於下面
    # splits = {}
    # for word in stats.keys():
    #     temp = []
    #     for i, c in enumerate(word):
    #         if i == 0:
    #             temp.append(c)
    #         else:
    #             temp.append(f'##{c}')
    #     splits[word] = temp

    return splits

compute_pair_scores: 統計所有 pair 的出現頻率與分數

def compute_pair_scores(stats, splits):
    letter_freqs = defaultdict(int)
    pair_freqs = defaultdict(int)

    for word, freq in stats.items():
        split = splits[word]
        if len(split) == 1:
            letter_freqs[split[0]] += freq
            continue
        for i in range(len(split) - 1):
            pair = (split[i], split[i + 1])
            letter_freqs[split[i]] += freq
            pair_freqs[pair] += freq

        letter_freqs[split[-1]] += freq

    scores = {
        pair: freq / (letter_freqs[pair[0]] * letter_freqs[pair[1]])
        for pair, freq in pair_freqs.items()
    }

    return scores

merge_pair: 選出分數最高的 pair 進行合併

def merge_pair(a, b, stats, splits):
    for word in stats:
        split = splits[word]
        if len(split) == 1:
            continue

        i = 0
        while i < len(split) - 1:
            if split[i] == a and split[i + 1] == b:
                merge = a + b[2:] if b.startswith("##") else a + b
                split = split[:i] + [merge] + split[i + 2 :]
            else:
                i += 1
        splits[word] = split
    return splits

因為不希望篇幅太長,這邊就以 WordPiece 為實作,有興趣可以再完成 bpe 的部分,本系列會參考大量的文章,文章內容寫的都很不錯,如果想更進一步可以看看,或者參考我整理過後的版本,目的是希望你不要被這篇文章侷限,而是應該要多看多學。
今天就介紹到這囉 ~~


上一篇
Day2: 從 0 訓練 26M GPT
下一篇
Day4: embedding & attention 觀念
系列文
實戰派 AI 工程師帶你 0->18
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言